#   converge2sweep.pl       Convert 'converge' commands to equivalent 'sweep' commands
#                           to reduce run time.
#
#   converge2sweep.pl [-d[level]] [-c] [-s] [-a] [-fXXXX[.XXX]]  inputfile [inputfile...]
#
#   -d              debug  level=0-5
#
#   -c              generated comments  & preserve original comments
#
#   -s              suppress summary of conversion statistics
#
#   -a              audit each discreet frequency needed (slow and memory-intensive operation, for debugging)
#
#   -fXXXX[.XXX]    follow a specific (exact) frequency through sweep reduce and concat actions to final sweep table entry
#                   This is always used in conjuction with frequency auditing, and the -f switch forces the -a switch 
#
#   combined output to STDOUT,  debugging info to STDERR
#   input from STDIN if no inputfiles specified

$audit = 0;             # perform an *expensive* detailed audit of each discreet frequency after the sweeps are output
                        # this may require memory use of 256mb or more.  Normal runs seldom use more than 3 or 4mb for worst case.

$follow = undef;        # a specific frequency to follow through the reduce/split/join logic.
                        # a fractional freq is allowed because, while start and end freqs must be integers, the step may still
                        # generate fractional freqs.  e.g.  12345 12346 .2  generates: 12345, 12345.2, 12345.4, 12345.6, 12345.8, and 12345

$debug = 0;             # debug level 0=normal, 1=some, 2=more, 3=lots, etc.

$nocomment = 1;         #default is no comments
                        # -c preserves the original input as comments and adds default commentary about conversion
                        # When comments are enabled, old code and commentary is indented.
                        # Only executable code (original or generated) is left-justified

$nosummary = 0;         #default is summary   -s disables the summary

$pending_converge_line = $pending_dwell_line = 0;
$nctime = 0;             #non-converted  time;

$step = $pctg = undef;  #initial step set to invalid value
$dwell = 180;           #default dwell
$basefreqs = 0;         #convertable base frequencies encountered
$filegrp = undef;

@rngs = ();             #accumulated ranges to put in sweeps ([0]$b=beg, [1]$e=end, [2]$s=step)  - temporarily accumulates ranges between other commands
@sweeps = ();           #all sweeps ([0]$b=beg, [1]$e=end, [2]$s=step, [3]$d=duration)   -  this is the *big* table which gets reduced
%aud = ();              #audit hash


parse_switches();
$logfilenames = 1 if ($#ARGV > 1);
#MAIN LOOP
while (<>)
{   if ($logfilenames and !$nocomment and ($prevfilename ne $ARGV) )
    {   # support multiple input files
        print "### START file: $ARGV\n" ;
        $prevfilename = $ARGV;
        $filegrp = 0;
    }
    $line++ ;
    chomp;
    s/#.*// if $nocomment;     #strip all comments
    # dwell command
    if ( /\s*dwell\s+([0-9.]+)/i )
    {
        print STDERR "\nDBG:L$line dwell=$1\trngs=$#rngs\n" if $debug >= 3;   #DEBUGGING
        if ($#rngs > 0)                             # dump any existing ranges into sweeps before changing dwell
        {
            insert_ranges(@rngs);
            @rngs=();
        }
        $dwell = $1;
        print "\t#$line $_\n" unless $nocomment;
        $pending_dwell = $_ ;
        $pending_dwell_line = $line;
    }
    # converge command
    elsif ( /^\s*(converge|fuzz)\s+(([0-9\.]+)(\%?))\s+([0-9\.]+)/i )
            #    1-------------1   23--------34---42   5--------5
    {
        $rng = $3;
        $pctg = ($4 eq '%');
        $step = $5;

        print "\t#$line $_\n" unless $nocomment;
        $pending_converge = $_ ;
        $pending_converge_line = $line;
        print STDERR  "\nDBG:L$line converge rng=$rng\tpctg=$pctg\tstep=$step\trng=$#rngs\n" if $debug >= 3;   #DEBUGGING
    }
    # frequency list - accumulate ranges for every frequency on the line
    elsif ( /^\s*[0-9]/ )
    {
        @freqs = split ;
        my $tmp = $#freqs + 1;
        print "\t#$line $_\n" unless $nocomment;
        print STDERR "\nDBG:L$line ($tmp ranges) " if $debug >= 3;   #DEBUGGING
        foreach $i (@freqs)                             # expand each frequency and add it to rngs
        {   #integer frequency, convert this one
            if ( $i=~/^\s*[0-9]+\s*$/ )
            {   my $n = $i + 0;
                my $b = int(  $n -  (($pctg) ? ($n * $rng)/100.0+.5 : $rng)  ); #beg freq
                my $e = int(  $n +  (($pctg) ? ($n * $rng)/100.0+.5 : $rng)  ); #end freq
                my $stp = $step;                                                #step
                print STDERR  "\n\t[$n] from $b to $e by $stp\t"  if $debug >= 3;   #DEBUGGING
                if ($b<=0 || $e<=0 || $stp>($e-$b) || $e<$b || round3($stp)==0)
                {
                    print "\n#$line $_\n# *** INVALID RANGE LINE $line: $i $rng ($pctg) from $b to $e by $stp\n" ;
                    die "INVALID RANGE";
                }
                push @rngs, [$b, $e, $stp, $line];
                $basefreqs++;
            }
            #not an integer freq - don't convert, preceeded by any pending dwell or converge
            elsif ( $i=~/^\s*[0-9\.]+\s*$/ )
            {   # drop any pending dwell or converge commands
                if ($pending_dwell_line > 0)
                {   print "$pending_dwell";
                    print "\t\# carried forward from line $pending_dwell_line" unless $nocomment;
                    print "\n";
                    $pending_dwell_line = 0;
                }
                if ($pending_converge_line > 0)
                {   print "$pending_converge";
                    print "\t\# carried forward from line $pending_converge_line" unless $nocomment;
                    print "\n";
                    $pending_converge_line = 0;
                }
                print "$i";
                print "\t\t\t# extracted from line $line" unless $nocomment;
                print "\n";
                $nctime += $dwell;       #accum a  total of non-converted times
            }
            elsif ( $i=~/^\s*#/ )       #comments end the list
            {
                last;
            }
            else
            {
                die "Invalid frequency list line $line";
            }
        }
        print STDERR "\nDBG:rngs=$#rngs\n" if $debug >= 3;   #DEBUGGING
    }
    #comment, print, or empty lines - send these immediately to output  Don't add any comments or line numbers
    elsif ( /^\s*(\#|print|$)/ )
    {
        print "$_\n" unless /^\s*$/ ;   #omit if entirely blank
    }
    #anything else, convert whatever we've got so far - then print the command
    else
    {
        convert();
        print "$_";
        print "\t\t\t#$line" unless $nocomment;
        print "\n";
    }
}
# END MAIN LOOP

convert();  #always try a convert at the end

######################################  END OF MAIN  ######################

###################################### CONVERSION REDUCTION/COMBINE/SPLIT ROUTINES ######################
sub convert
{
    if ($#rngs >= 0)                     # first, dump any existing ranges into sweeps before proceeding
    {
        insert_ranges(@rngs);
        @rngs=();
    }

    return if ($#sweeps < 1);                                   # nothing to convert

    print STDERR "\nDBG:START $#sweeps SWEEPS time=" . total_time() . " sec\n" if $debug >= 1;   #DEBUGGING
    my $after = 1;
    my $before = total_time();
    my $curr = 0;
    while (round3($curr) != round3($after))
    {
        $curr = $after;
        reduce();                                           # this is the meat - reduce all overlapping sweeps or contained
        concat();                                           # merge any adjacent records which can be a single contiguous range
        $after = total_time();                              # until we get no more benefit
    }
    print STDERR "\nDBG:REDUCED TO $#sweeps SWEEPS time=" . total_time() . " sec\n" if $debug >= 1;   #DEBUGGING
    my $after = total_time();
    my $reduct = ($after - $before) * 100 / $before;
#    dump_sweeps(0);
    printf  "\t# before: %-.2f sec%s => after: %-.2f sec%s\n\t# reduction: %-.2f%s%s \"%s\" %d\n"
            . "\t# + freqs not converted: %-.2f sec%s, %-.2f%s total group runtime\n",
        $before, format_elapsed($before), $after, format_elapsed($after), $reduct, '%', format_elapsed($before-$after), $ARGV, $filegrp,
        $nctime, format_elapsed($nctime), ($nctime+$after), format_elapsed($nctime+$after) unless $nosummary;
    dump_sweeps(0);
    @sweeps = ();
    $basefreqs = $before = $after = $nctime = 0;              #reset all stats

    }

sub insert_ranges   #insert a bunch of accumulated ranges (in @rngs) into the @sweeps table
{
    print STDERR "\nDBG:inserting $#rngs ranges dwell=$dwell" if $debug >= 3;   #DEBUGGING
    foreach $i (@_)
    {   my ($b, $e, $stp, $ln) = @$i;       # beg, end, step, lineno
        $stp = round3($stp);                # 3 digit accuracy
        my $dur = round3($dwell / count_steps($b,$e,$stp)) ;
        if ($b<=0 || $e<=$b || $stp<=0 || $dur<=0)
            {   print "$b to $e step $stp duration $dur\n";
                die "Invalid freq range generated";
            }
        push @sweeps, [$b, $e, $stp, $dur];
        print STDERR "\n\t[$b|$e|$stp|$dur] " if $debug >= 4;   #DEBUGGING
        if ($audit or $follow)
        {   for (my $i = $b ; $i <= $e ; $i = round3($i + $stp) )
            {
                print STDERR "NEED $i for $dur ($b-$e s$stp) line $ln\n" if ($follow and $i == $follow);  #FOLLOWING
                if ($audit)
                {
                    $aud{$i}=$dur if (!$aud{$i} or $aud{$i} < $dur); # add or increase the required duration in %aud
                }
            }
        }
    }
    print STDERR  "\nDBG:sweeps=$#sweeps\n" if $debug >= 3;   #DEBUGGING
}

sub reduce      #for each record in @sweeps, try to reduce it by combining with nearby records
{
    print STDERR "\nDBG:UNSORTED $#sweeps SWEEPS time=" . total_time() . " sec\n" if $debug >= 2;   #DEBUGGING

    print STDERR "performing DOWNWARD reductions\n" if $debug >= 2;   #DEBUGGING
    sort_by_end();
    print STDERR "\nDBG:SORTED (by end [1]) $#sweeps SWEEPS\nbeg   \tend   \tstep\tdur\n" if ($debug >= 2);   #DEBUGGING
    dump_sweeps(1) if ($debug >= 3)   ;   #DEBUGGING

    for (my $x = 0 ; $x <= $#sweeps ; $x++)
    {   for (my $y = $x-1 ; $y >= 0 && overlap($y, $x) ; $y--)
        {
             check_reduce( $x, $y );
             check_reduce( $y, $x );        # try it both ways
        }
    }

    print STDERR "\nDBG:performing UPWARD reductions\n" if $debug >= 2;   #DEBUGGING
    sort_by_beg();
    print STDERR "\nDBG:SORTED (by beg [0]) $#sweeps SWEEPS\n" if ($debug >= 2);   #DEBUGGING
    dump_sweeps(1) if ($debug >= 3)   ;   #DEBUGGING
    for (my $x = 0 ; $x <= $#sweeps ; $x++)
    {   for (my $y = $x+1 ; $y <= $#sweeps && overlap($y, $x) ; $y++)
        {
             check_reduce( $x, $y );
             check_reduce( $y, $x );        # try it both ways
        }
    }
    for (my $x = 0 ; $x <= $#sweeps ; $x++)
    {   for (my $y = $x+1 ; $y <= $#sweeps && overlap($y, $x) ; $y++)
        {
             check_split( $x, $y );         # split records if $y falls entirely within $x (UPWARD only)
        }
    }
}

sub check_reduce    #check two records (X and Y) - if Y does some of the same frequencies as X for at least as long with same step - reduce X.
{   my ($xindex, $yindex) = @_;
    my ($xb, $xe, $xs, $xd) = @{$sweeps[$xindex]};     # beg, end, step, duration - X the one we are considering
    my ($yb, $ye, $ys, $yd) = @{$sweeps[$yindex]};     #  "                       - Y the one which might overlap X

# Dont attempt reduction if either is logically deleted
    return if (($xd == 0 || $yd == 0));

# don't handle this case - Y is completely contained within X, because we only want to change X, not Y
# presumably we will merge Y into X (instead of X into Y) in an check_split call
    return if ( $xb < $yb && $xe > $ye );

# Y is only useful to us if X's step is the same as, or a multiple of, Y's step.
# If it is a multiple, it will include the same frequencies
# Modulus needs to be integer, so scale the step by 3 digits and convert to int
    return if ( int(round3($xs)*1000) % int(round3($ys)*1000) != 0);
#   return if ( $ys != $xs);

# only combine with a same or greater duration
    if ( ($yd >= $xd) )
    # remove any part of X which is already COVERED by Y
    {
        print STDERR "\tX$xindex=[$xb-{$xe}x$xs\@$xd] Y$yindex=[$yb-{$ye}x{$ys}\@$yd]" if $debug >= 2;   #DEBUGGING

        my $newb = $xb;
        my $newe = $xe;
        my $news = $xs;
        my $newd = $xd;

        $newb = $ye+$xs if ($xb >= $yb);
        $newe = $yb-$xs if ($xe <= $ye);
        $newd = 0.0 if ($newb > $newe);                    # if Y includes *all* of X, set X's duration to zero to logically delete it
        $sweeps[$xindex] = [$newb, $newe, $news, $newd] ;   # replace the X record in @sweeps

        print STDERR "\tX$xindex=>[$newb|$newe|$news|$newd]\n" if $debug >= 2;   #DEBUGGING
        print STDERR "REDUCE X[$xb-$xe s$xs $xd] Y[$yb-$ye s$ys $yd] \n" if ($follow and $xb<=$follow && $xe>=$follow);   #FOLLOWING
    }
}

sub check_split                                         # split X into two records if Y falls entirely within X
{   my ($xindex, $yindex) = @_;
    my ($xb, $xe, $xs, $xd) = @{$sweeps[$xindex]};     # beg, end, step, duration - X the one we are considering
    my ($yb, $ye, $ys, $yd) = @{$sweeps[$yindex]};     #  "                       - Y the one which might fall within X
# Dont attempt reduction if either is logically deleted
    return if (($xd == 0 || $yd == 0));

# Y is only useful to us if X's step is the same as, or a multiple of, Y's step.
# this caused trouble when X and Y have different steps
    return if ( int(round3($xs)*1000) % int(round3($ys)*1000) != 0);
#   return if ($ys != $xs);

# split X into two records if Y falls within X.  Trim the existing X to fall below Y and add a new record just above Y
    if ( ($yd >= $xd) && ($yb >= $xb) && ($ye <= $xe) )
    {
        print STDERR "\tX$xindex=[$xb|$xe|$xs|$xd] Y$yindex=[$yb|$ye|$ys|$yd]" if $debug >= 2;   #DEBUGGING

        my $newe = $yb - $xs;
        $sweeps[$xindex] = [$xb, $newe, $xs, $xd] ;   # replace the lower part of X record in @sweeps

        my $newb = $ye + $xs;
        push @sweeps, [$newb, $xe, $xs, $xd];          # add a new record to @sweeps

        print STDERR "\tX$xindex=>[$xb|$newe|$xs|$xd]\~[$newb|$xe|$xs|$xd]\n" if $debug >= 2;   #DEBUGGING
        print STDERR "SPLIT  X[$xb-$xe s$xs $xd] Y[$yb-$ye s$ys $yd] \n" if ($follow and $xb<=$follow && $xe>=$follow);  #FOLLOWING
    }
}

sub concat  #go through @sweeps checking the next higher (valid) record and concatenate them if they "touch"
{   sort_by_beg();
    my $mcnt = 0;
    for (my $x = 0 ; $x < $#sweeps ; $x++)
    {   my ($xb, $xe, $xs, $xd) = @{$sweeps[$x]};
        my $y = $x+1;
        while ($y <= $#sweeps && ${$sweeps[$y]}[3] == 0.0) { $y++; };           #skip over deleted records
        my ($yb, $ye, $ys, $yd) = @{$sweeps[$y]};
        if ($xd!=0.0 && $yd!=0.0 && $xs==$ys && $xd==$yd && $xe==($yb-$xs))    # if *same* step (no multiples) and duration and Y starts 1 step after X
        {   $mcnt++;
            $sweeps[$x]   = [$xb, $ye, $xs, $xd];      # update X to cover the full range
            $sweeps[$y]   = [$yb, $ye, $ys, 0.0];      # delete Y by setting duration to zero
            print STDERR "CONCAT X[$xb-$xe s$xs $xd] Y[$yb-$ye s$ys $yd] \n" if ($follow and $xb<=$follow && $ye>=$follow);  #FOLLOWING
        }
    }
    print STDERR "\nDBG:$mcnt adjacent records merged\n" if $debug >= 1;   #DEBUGGING
    return $mcnt;
}

######################################  OTHER ROUTINES ######################

sub total_time      #Calculate the total processing time of all the records in @sweeps
{   my $tot= my $cnt = my $loopcnt = 0;
    foreach $t (@sweeps)
    {   my ($b, $e, $s, $d) = @{$t};
        $loopcnt = ($e - $b)/$s + 1;
        $tot += $d * $loopcnt;
        $cnt++ if ( $d == 0 );
    }
    print STDERR "\nDBG:$cnt deleted (zero duration) records\n" if $debug >= 1;   #DEBUGGING
    return $tot;
}

sub overlap         #Check two record (X and Y), return true if they overlap
{
    my ($xindex, $yindex) = @_;
    my ($xb, $xe, $xs, $xd) = @{$sweeps[$xindex]};      # beg, end, step, duration - X the one we are considering
    my ($yb, $ye, $ys, $yd) = @{$sweeps[$yindex]};      #  "                       - Y the one which might overlap X

    return 1 if ( ($xb >= $yb) && ($xb <= $ye) );       # overlap, X beginning is within Y
    return 1 if ( ($xe >= $yb) && ($xe <= $ye) );       # overlap, X end is within Y
    return 1 if ( ($yb >= $xb) && ($ye <= $xe) );       # Y is fully contained in X
    return 1 if ( ($yb <= $xb) && ($ye >= $xe) );       # X is fully contained in Y
    return 0;
}

sub dump_sweeps()   #dump out all the (non-zero-duration) records in @sweeps
# @_ is true for debug output (i.e. to STDERR), false for real (i.e. the final output)
{   my ($dbgdump) = @_ ;
    my @finals = ();
#    sort_by_tot_dur() if (!$dbgdump);               #sort by total dur for the final output, so we can group them by dwell  Nope!
#    sort_by_beg();                                  # to get beg freq in ascending order.
    sort_by_dur();                                   # reduce number of dwell cmds
    my $OUTTYPE = ($dbgdump) ? STDERR : STDOUT;
    my $prevdwl = 0;
    print $OUTTYPE "#dbg:" if $dbgdump;                  #don't let multiple dumps look like real output when debuging
    print $OUTTYPE "\t# beg     end    step\n"  unless $nocomment;
    my $tcnt = my $prevb = 0;
    for (my $i = 0 ; $i <= $#sweeps ; $i++)
    {   my ($b, $e, $stp, $dur) = @{$sweeps[$i]};
        if ($dur > 0.0)
        {   my $fcnt = count_steps($b, $e, $stp);
            my $tim = $fcnt * $dur;

#           my $dwl = round3($fcnt * $dur);         #nope!  dwell is each freq for sweep cmd (unlike converge)
            my $dwl = $dur;

            my $gap = $b - $prevb;
            $gap = 0 if ($dwl != $prevdwl);     #only show gaps within a dwell

            $tcnt++;
            print $OUTTYPE "#dbg:  " if $dbgdump;         #don't let multiple dumps look like real output when debuging
            printf $OUTTYPE "dwell %-g\n", $dwl if ($dwl != $prevdwl);
            print $OUTTYPE "#dbg:  " if $dbgdump;         #don't let multiple dumps look like real output when debuging
            printf $OUTTYPE "sweep %-7d %-7d %-g ", $b, $e, $stp;

            printf $OUTTYPE "   #s%d (%+d) %g frqs at %gs = %gs", $i, $gap, $fcnt, $dur, $tim unless $nocomment;
            push @finals, "FINAL s$i contains $follow => $dur step $stp\n" 
                if ($follow and ($b<=$follow && $e>=$follow) and (($follow-$b)*1000)%($stp*1000)==0);  #FOLLOWING
            print $OUTTYPE "\n";
            $prevb = $e;
            $prevdwl = $dwl;
        }
    }
    foreach $f (@finals) 
    {
        print STDERR $f;
    }
    print "\t# generated $tcnt sweeps from $basefreqs base frequencies\n" unless $nocomment;
    do_audit() if $audit;
    %aud = () unless $dbgdump;      #empty the audit table if this is final output
    $filegrp++ unless $dbgdump;     #number groups if this is final output

}   

sub round3  #round @_ to 3 decimal places
{
    return (int( 1000.0 * $_[0] + .5 ) / 1000.0) ;
}

sub sort_by_beg     #Sort @sweeps by beginning frequency
{
    @sweeps =
        sort            #sort by beg, end for upward search for overlapps
        {
            ($a->[0] == $b->[0]) ? ($a->[1] <=> $b->[1]) : ($a->[0] <=> $b->[0])
        }
        @sweeps  ;
}

sub sort_by_end     #Sort @sweeps by ending frequency
{
    @sweeps =
        sort            # sort by end, beg for downward search
        {
            ($a->[1] == $b->[1] ) ? ($a->[0] <=> $b->[0] ) : ($a->[1] <=> $b->[1] )
        }
        @sweeps  ;
}
sub sort_by_dur     #Sort @sweeps by ending frequency
{
    @sweeps =
        sort            # sort by end, beg for downward search
        {
            ($a->[3] == $b->[3] ) ? ($a->[0] <=> $b->[0] ) : ($a->[3] <=> $b->[3] )
        }
        @sweeps  ;
}

#sub sort_by_tot_dur     #Sort @sweeps by total dwell for each record
#{
#    @sweeps =
#        sort            # sort by dwell , beg for final output
#        {
#            my ($xb, $xe, $xs, $xd) = @{$a};
#            my ($yb, $ye, $ys, $yd) = @{$b};
#            my $xdwl = round3(count_steps($xb,$xe,$xs) * $xd);
#            my $ydwl = round3(count_steps($yb,$ye,$ys) * $yd);
#            ($xdwl == $ydwl) ? ($xb <=> $yb) : ($xdwl <=> $ydwl)
#        }
#        @sweeps  ;
#}

sub parse_switches  #Collect switches from the command line, remove them from ARGV file list
{   while ( $ARGV[0] =~/^(-|\/)/ )                  # get switches (leading arguments starting with - or /) from command-line
    {   my $sw = shift @ARGV;
        if ( $sw =~ /^(-|\/)c$/i)                    # comment switch -c or -C ( or /c or /C)
        {
            $nocomment = 0;
        }
        elsif ( $sw =~ /^(-|\/)s$/i)                    # no-summary switch -s or -S ( or /s or /S)
        {
            $nosummary = 1;
        }
        elsif ($sw =~ /^(-|\/)d([0-5]?)/i)              # debug level  -d or -D increments it, -dN or -DN sets it to N
        {
            $debug = ($2) ? ($2) : $debug+1;   #DEBUGGING
            print STDERR "DEBUG level set to $debug\n";   #DEBUGGING
        }
        elsif ( $sw =~ /^(-|\/)a$/i)                    # audit switch -a or -A ( or /a or /A)
        {
            $audit = 1;
        }
        elsif ($sw =~ /^(-|\/)f([0-9.]+)/i)             # follow switch
        {
            die "-f switch must be followed by a frequency, e.g.  -f44632 or -f115322.2" if !$2;
            $follow = $2 + 0;
            $audit = 1;
        }
        else
        {
            die "invalid switch $sw";
        }
    }
}

sub count_steps     #($beg, $end, $stp) - because I just couldn't remember to do this correctly & consistently in each routine
{
    my ($b, $e, $stp) = @_;
    return (($e-$b)/$stp) + 1;
}

sub format_elapsed  # make a large number of seconds easier to read
{   my ($t) = @_;
    $t = int ($t + 0.5);
    return "" if $t < 61;
    my $d = int($t / 86400);
    $t %= 86400;
    my $h = int($t / 3600);
    $t %= 3600;
    my $m = int($t / 60);
    $t %= 60;
    my $res = " (";
    $res .= $d ."d " if $d;
    $res .= $h . "h " if $h;
    $res .= $m . "m" if ($d or $h or $m);
#    $res .=$t . "s" if $t;
    $res .= ')';
    return ")" if ($d == 0 && $h == 0 && $m==0);
    return $res;
}

sub do_audit
{   my %swp;
    print "*** starting audit pass for $#sweeps sweeps\n";
    for (my $i = 0 ; $i <= $#sweeps ; $i++)
    {
        my ($b, $e, $s, $d) = @{$sweeps[$i]};
        for (my $z = $b ; $z <= $e ; $z=round3($z + $s))    #round incremental freqs to 3 places to ensure match
        {   $swp{$z} = $d if (!$swp{$z} or $swp{$z} < $d ); # add or increase actual duration in %swp
            print STDERR "AUDIT s$i entry $z => $d step $sweeps[$i]->[2]\n" if ($follow and $z == $follow);  #FOLLOWING
            $zcnt++;
        }
    }
    printf "%d discrete sweep freqs\n", $zcnt;
    @u = sort keys %aud;
    printf "%d discrete audit freqs\n", $#u;

    foreach my $af (@u)
    {   my $ad = $aud{$af};
        my $bd = $swp{$af};
        my $ap = 0;
        if (!$bd)
        {   print "AUDIT FAILURE for freq: $af dur: $ad prev: $ap\n";
            die "AUDIT - sweep freq failure\n";
        }
        if ($bd < $ad)
        {
            print "AUDIT FAILURE for freq: $af dur: $ad, sweep has $bd\n";
            die "AUDIT - insuf duration\n";
        }
        $ap = $af;
    }
    print "end audit pass - OK\n";
}